<?php
#
# dmBridge: a data access framework for CONTENTdm(R)
#
# Copyright © 2009, 2010, 2011 Board of Regents of the Nevada System of Higher
# Education, on behalf of the University of Nevada, Las Vegas
#

/**
 * Encapsulates a CONTENTdm(R) search query.
 *
 * @author Alex Dolski <alex.dolski@unlv.edu>
 * @license http://www.opensource.org/licenses/mit-license.php
 */
class DMObjectQuery extends DMAbstractQuery implements DMQuery,
		DMURIAddressable {

	const DEFAULT_LIMIT = 20;

	/** @var DMObjectQuery */
	private static $current;

	/** @var array Array of DMFacetTerms, cached by getSearchResults() */
	private $facet_terms = array();
	/** @var float */
	private $max_rating;
	/** @var float */
	private $min_rating;
	/** @var array */
	private $sort_fields = array();
	/**
	 * @var If >0, will be overwritten by the suggestion resulting from
	 * getSearchResults().
	 */
	private $suggestion = 1;


	/**
	 * @return DMObjectQuery
	 * @deprecated
	 */
	public static function getCurrent() {
		return self::$current;
	}

	/**
	 * @param DMObjectQuery oq
	 * @deprecated
	 */
	public static function setCurrent(DMObjectQuery $oq) {
		self::$current = $oq;
	}

	public function __construct() {
		parent::__construct();
		$this->setNumResultsPerPage(self::DEFAULT_LIMIT);
	}

	/**
	 * @param DMFacetTerm f
	 */
	public function addFacetTerm(DMFacetTerm $f) {
		$this->facet_terms[] = $f;
	}

	/**
	 * @return array of DMFacetTerms
	 */
	public function getFacetTerms() {
		return $this->facet_terms;
	}

	/**
	 * Convenience method equivalent to calling
	 * <code>getURI(DMBridgeComponent::TemplateEngine, "atom")</code>.
	 *
	 * @return DMInternalURI
	 */
	public function getFeedURI() {
		return $this->getURI(DMBridgeComponent::TemplateEngine, "atom");
	}

	/**
	 * @return array Indexed array of floats with min rating at position 0 and
	 * max rating at position 1
	 */
	public function getRatingRange() {
		return array($this->min_rating, $this->max_rating);
	}

	/**
	 * If called, this query instance will search <strong>only</strong> by
	 * rating.
	 *
	 * @param float min Float on a scale of 0-1
	 * @param float max Float on a scale of 0-1
	 * @throws DMIllegalArgumentException if min > max
	 */
	public function setRatingRange($min, $max) {
		if ($min > $max) {
			throw new DMIllegalArgumentException(
					DMLocalizedString::getString("RATING_LARGER_THAN_MAX"));
		}
		$this->min_rating = abs($min);
		$this->max_rating = abs($max);
	}

	/**
	 * @return Array of DMObjects
	 * @throws DMInternalErrorException
	 */
	public function getSearchResults() {
		if ($this->min_rating !== null && $this->max_rating !== null) {
			return $this->getSearchResultsByRating();
		} else {
			return $this->getCdmSearchResults();
		}
	}

	private function getSearchResultsByRating() {
		$ds = DMDataStoreFactory::getDataStore();
		$total = 0;
		$this->results = $ds->getHighestRatedObjects(
				$this->getCollections(), $this->getPage(),
				$this->getNumResultsPerPage(), $total);
		$this->num_results = $total;
		return $this->results;
	}

	/**
	 * @return Array of DMObjects
	 * @throws DMInternalErrorException
	 */
	private function getCdmSearchResults() {
		if (count($this->results)) {
			return $this->results;
		}

		$terms = $this->getPredicates();

		// transform $terms into the format required by dmQuery()
		$dmqterms = $field = array();
		$count = count($terms);
		$cdm_no_likey = array(" and ");

		$stopwords_pathname = $_SERVER['DOCUMENT_ROOT'] . "/stopwords.txt";
		$stopwords = array();
		$fh = null;
		if (file_exists($stopwords_pathname)) {
			$fh = fopen($stopwords_pathname, "r");
			$tmp = fread($fh, filesize($stopwords_pathname));
			$stopwords = explode("\n", $tmp);
		}

		for ($i = 0; $i < $count; $i++) {
			if (!$terms[$i]->isValid()) {
				continue;
			}
			$field[$i] = (strtolower($terms[$i]->getField()->getNick()) == "any")
				? "CISOSEARCHALL" : $terms[$i]->getField()->getNick();

			// remove all stop words
			foreach ($stopwords as $stopword) {
				$terms[$i]->setString(
						str_replace(" " . $stopword . " ", "", $terms[$i]->getString()));
			}

			if (!$terms[$i]->getString()) {
				continue;
			}
			$dmqterms[$i] = array(
				'field' => $field[$i],
				'string' => str_replace($cdm_no_likey, "", $terms[$i]->getString()),
				'mode' => $terms[$i]->getMode()
			);
		}

		if ($fh) {
			fclose($fh);
		}

		$aliases = array();
		foreach ($this->getCollections() as $c) {
			if ($c->getAlias() == "/dmdefault") {
				$aliases = array("all");
				break;
			}
			$aliases[] = $c->getAlias();
		}

		if (count($aliases) < 1) {
			$aliases = array("all");
		}

		$objects = $this->getObjects();
		$ptr = count($objects) ? $objects[0]->getPtr() : -1;
		$suppress = ($ptr > -1) ? 0 : 1;

		$collections = $results = array();
		$sort_fields = $this->transformSortFieldsForCdm();

		// dmQuery() will modify $facets by reference
		$facets = array();
		foreach ($this->getFacetTerms() as $f) {
			$facets[] = $f->getField()->getNick();
		}
		$facets = implode(":", $facets);
		$facets_to_compare = $facets;

		$this->suggestion = 1;

		$results = dmQuery($aliases, $dmqterms, array('title'),
			$sort_fields, $this->getNumResultsPerPage(),
			$this->getStart() + 1, $this->num_results, $suppress, $ptr,
			$this->suggestion, $facets);
		// see comment in previous case statement
		foreach ($results as $r) {
			$collections[] = DMCollectionFactory::getCollection($r["collection"]);
		}
		// dmQuery may or may not change $facets; if not, don't try to
		// instantiate facets
		if ($facets != $facets_to_compare) {
			$this->facet_terms = $this->instantiateFacetsFromCdmXML(
					$facets, $collections);
		}

		// Assemble results
		$i = 0;
		foreach ($results as $r) {
			$this->results[] = DMObjectFactory::getObject(
					$collections[$i], $r['pointer']);
			$i++;
		}
		return $this->results;
	}

	/**
	 * Instantiates an array of DMFacetTerms based on the XML tag soup returned
	 * by dmQuery().
	 *
	 * @param string xml
	 * @param array collections array of DMCollection objects
	 * @return array of DMFacetTerm objects
	 */
	private function instantiateFacetsFromCdmXML($xml, array $collections) {
		if (empty($xml)) {
			return array();
		}
		// dmQuery() returns invalid XML. Who knows if there any other
		// entities... or angle brackets (!!)... we need to escape.
		$xml = str_replace('&', '&amp;', $xml);

		if (count(array_unique($collections)) > 1 || count($collections) < 1) {
			$collections = array(DMCollectionFactory::getCollection("/dmdefault"));
		}

		$dxml = new DOMDocument('1.0', 'utf-8');
		if (!$dxml->loadXML($xml)) {
			return array();
		}
		// I guess we'll just assume that there are equal numbers of each tag...
		$labels = $names = $counts = array();
		foreach ($dxml->documentElement->getElementsByTagName('label') as $node) {
			$labels[] = $node->nodeValue;
		}
		foreach ($dxml->documentElement->getElementsByTagName('name') as $node) {
			$names[] = $node->nodeValue;
		}
		foreach ($dxml->documentElement->getElementsByTagName('count') as $node) {
			$counts[] = $node->nodeValue;
		}
		$facets = array();
		$count = count($names);
		for ($i = 0; $i < $count; $i++) {
			if (!is_object($collections[0]->getField($labels[$i]))) {
				continue;
			}
			$field = clone $collections[0]->getField($labels[$i]);
			// If the facet name is already part of the query, skip it
			foreach ($this->getPredicates() as $st) {
				if ($st->getString() == $names[$i]) {
					continue(2);
				}
			}
			// The facet names are all lowercase!!!!
			$field->setValue(ucwords($names[$i]));
			$ft = new DMFacetTerm($field, $counts[$i]);

			$q = clone $this;
			$st = new DMQueryPredicate();
			$st->setString($names[$i]);
			$st->setField($field);
			$st->setMode("all");
			$q->addPredicate($st);

			$ft->setURI($q->getURI());
			$facets[] = $ft;
		}
		return $facets;
	}

	/**
	 * @param int rpp Results per page
	 * @Override
	 */
	public function setNumResultsPerPage($rpp) {
		// cdm PHP API defines 1024 as the max
		$this->results_per_page = ($rpp > 1024) ? 1024 : (int) abs($rpp);
	}

	/**
	 * @return Boolean True if any search terms are present, or
	 * false if not.
	 */
	public function arePredicates() {
		$terms = $this->getPredicates();
		if (count($terms) >= 1 && $terms[0] instanceof DMQueryPredicate) {
			return (!$terms[0]->isBrowse());
		}
		return false;
	}

	/**
	 * @param array fields Associative array of field-direction pairs, e.g.
	 * "title" => "asc" or "descri" => "desc"
	 * @Override
	 */
	public function setSortFields(array $fields) {
		// if invalid fields are provided, default to sorting by title ascending
		$keys = array();
		foreach ($fields as $field => $direction) {
			$keys[] = $field;
		}
		if (count($fields) < 2 || !$keys[0]) {
			$this->sort_fields = array("title" => "asc");
		}
		$this->sort_fields = $fields;
	}

	/**
	 * Returns the suggestion generated for an apparently misspelled term with
	 * no results; e.g. "airplaen" -> "Did you mean 'airplane'?"
	 *
	 * @return string
	 */
	public function getSuggestion() {
		return $this->suggestion;
	}

	private function transformSortFieldsForCdm() {
		$transformed = array();
		foreach ($this->getSortFields() as $field => $direction) {
			if ($direction == "desc") {
				$transformed[] = "reverse";
			} else {
				$transformed[] = $field;
			}
		}
		return $transformed;
	}

	/**
	 * @param int component One of the constants in the DMBridgeComponent class
	 * @param string representation A valid representation, such as "html" or
	 * "atom"
	 * @param DMTemplateSet ts
	 * @param DMInternalURI inherited_uri An optional URI from which to
	 * "inherit" a query and fragment.
	 * @return DMInternalURI URI of the query
	 */
	public function getURI($component = DMBridgeComponent::TemplateEngine,
			$representation = null, DMTemplateSet $ts = null,
			DMInternalURI $inherited_uri = null) {
		$terms = $this->getPredicates();
		$collections = $this->getCollections();
		$qs = array();
		// page
		if ($this->getPage() > 1) {
			$qs['page'] = $this->getPage();
		}
		// limit
		if ($ts && $this->getNumResultsPerPage()
				!= $ts->getNumResultsPerPage()) {
			$qs['rpp'] = $this->getNumResultsPerPage();
		}

		// query predicates
		for ($i = 1; $i <= count($terms); $i++) {
			if ($terms[$i-1]->getField()) {
				$qs['CISOOP' . $i] = $terms[$i-1]->getMode();
				$qs['CISOFIELD' . $i] = $terms[$i-1]->getField()->getNick();
				$qs['CISOBOX' . $i] = $terms[$i-1]->getString();
			}
		}
		// collections
		$params = $component == DMBridgeComponent::HTTPAPI
			? "api/" . DMBridgeVersion::getLatestHTTPAPIVersion() : "objects";

		$parts = DMHTTPRequest::getCurrent()->getURI()->getParamComponents();
		if (count($parts) > 1 && $parts[0] == "objects"
				&& DMCollection::exists("/" . $parts[1])) {
			$params .= "/" . $parts[1];
		} else {
			$aliases = array();
			for ($i = 1; $i <= count($collections); $i++) {
				$aliases[] = $collections[$i-1]->getAlias();
			}
			$qs['CISOROOT'] = implode(",", $aliases);
		}

		if (!array_key_exists("CISOBOX1", $qs)) {
			if (array_key_exists("CISOROOT", $qs)) {
				unset($qs['CISOROOT']);
			}
		} else if (count($collections)) {
			$qs['CISOROOT'] = $collections[0];
		}

		if ($inherited_uri) {
			$inherited_uri = clone $inherited_uri;
			$inherited_uri->setParams($params);
			foreach ($qs as $key => $value) {
				$inherited_uri->setQueryValue($key, $value);
			}
			$inherited_uri->setExtension($representation);
			return $inherited_uri;
		}
		return DMInternalURI::getURIWithParams($params, $qs, $representation);
	}

}
